package jm.easyconfig;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.StringUtils;
import org.springframework.web.context.support.ServletContextPropertySource;

import jm.easyconfig.MySqlConfigSource.ChangeManageArgs;
import jm.easyconfig.MySqlConfigSource.ChangeSubmitArgs;

public class CfgAdminServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

	private static ThreadLocal<ThreadContextBean> threadContext = new ThreadLocal<ThreadContextBean>();

	static ThreadContextBean threadContext() {
		ThreadContextBean ctx = threadContext.get();
		if (ctx == null) {
			ctx = new ThreadContextBean();
			threadContext.set(ctx);
		}

		return ctx;
	}

	static class ThreadContextBean extends HashMap<String, Object> {
		private static final long serialVersionUID = 1L;

		private AdminUser user = null;

		public AdminUser getUser() {
			return user;
		}

		public String username() {
			return user != null ? user.username : "";
		}

		public void setUser(AdminUser user) {
			this.user = user;
		}
	}

	@Override
	protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		try {
			// support three pattern as /{servletpath}/{command} or
			// /{subsystem}/{servletpath}/{command} or /{command}
			String command = req.getRequestURI()
					.substring(req.getRequestURI().indexOf(req.getServletPath()) + req.getServletPath().length());

			Object ret = "";

			if ("/".equals(command))
				command = "/home.html";

			if ("/health".equals(command)) {
				ret = "OK";
			} else if ("/security-login".equals(command)) {
				ret = doLogin(req, resp);
			} else if ("/security-logout".equals(command)) {
				ret = doLogout(req, resp);
			} else if (command.startsWith("/lib/")) {
				doResourceResponse(command, resp);
				return;
			} else if ("/get-prop".equals(command)) {
				ret = doGetProp(req);
			} else if (!assureLogined(req, resp))
				return;

			if (".html".equals(getFileExt(command))) {
				doResourceResponse(command, resp);
				return;
			}

			// dispatch to restful service;
			switch (command) {
			case "/config-list":
				ret = doConfigList(req);
				break;
			case "/change-history":
				ret = doConfigHistory(req);
				break;
			case "/restore-version":
			case "/restore-change-before":
			case "/restore-change-after":
				ret = doRestore(req, command);
				break;
			case "/change-submit":
				ret = doChangeSubmit(req);
				break;
			case "/change-manage":
				ret = doChangeManage(req);
				break;
			case "/gray-publish":
				ret = doGrayPublish(req);
				break;
			case "/inspect":
				ret = doInspect(req);
				break;
			case "/system":
				ret = doSystem(CfgContextEnvironmentAware.environment);
				break;
			case "/environment":
				doEnvironment(resp);
				return;
			case "/list-to-load":
				ret = CfgContextEnvironmentAware.getSharedInstance().listToLoad((String) req.getParameter("profile"));
				break;
			case "/cipher":
				ret = doCipher(req);
				break;
			case "/item-updated-history":
				ret = CfgContextEnvironmentAware.configSource.itemUpdatedHistory(req.getParameter("item_code"));
				break;
			case "/security-change-password":
				ret = doChangePassword(req);
				break;
			case "/security-user-list":
				ret = doAdminUserList();
				break;
			case "/security-user-create":
				ret = doAdminUserCreate(CfgHelper.jsonRead(readBodyAsString(req), AdminUser.class));
				break;
			case "/security-user-put":
				ret = doAdminUserPut(req);
				break;
			case "/security-user-delete":
				ret = doAdminUserDelete(CfgHelper.jsonRead(readBodyAsString(req)));
				break;
			case "/security-login":
			case "/security-logout":
			case "/health":
				break;

			case "/nodes-gather":
				ret = doNodesGather(req);
				break;
			case "/nodes-get":
				ConfigItem item = CfgContextEnvironmentAware.configSource.loadConfigItem(SysProps.APPLICATION_NODES, SysProps.applicationNodes);
				ret = CfgHelper.jsonRead(item.item_value);
				break;
//			case "/change-get":
//				ret = doChangeGet(req);
//				break;
			default:
				resp.setStatus(404);
				return;
			}

			// deal with the return result
			HashMap<String, Object> result = new HashMap<String, Object>();
			result.put("error", 0);
			result.put("data", ret);
			result.put("context", configContext(req));
			resp.setContentType("application/json,charset=UTF-8");
			resp.getOutputStream().write(CfgHelper.jsonWrite(result).getBytes(StandardCharsets.UTF_8));
		} catch (Throwable e) {
			e.printStackTrace();
			String errmsg = e.getMessage();
			CfgHelper.logger.error("error ocurred. {}", errmsg);
			HashMap<String, Object> result = new HashMap<String, Object>();
			result.put("error", e.getClass().getSimpleName());
			result.put("data", errmsg == null ? e.getClass().getName() : errmsg);
			resp.setContentType("application/json,charset=UTF-8");
			resp.getOutputStream().write(CfgHelper.jsonWrite(result).getBytes(StandardCharsets.UTF_8));
		}

	}

	Object doNodesGather(HttpServletRequest req) {
		if (!(CfgContextEnvironmentAware.publisher instanceof RabbitmqUpdater)) {
			throw new ApplicationException("This action can be invoked only under rabbitmq udpater.");
		}

		RabbitmqUpdater updater = (RabbitmqUpdater) CfgContextEnvironmentAware.publisher;
		updater.nodesGather();
		return CfgContextEnvironmentAware.nodeName();
	}

	Object doInspect(HttpServletRequest req) {
		String itemCode = req.getParameter("code").trim();
		String node = req.getParameter("node");

		ConfigItem item = CfgContextEnvironmentAware.configSource.loadConfigItem(itemCode);
		if (item == null)
			item = (ConfigItem) new ConfigItem().withCode(itemCode);

		LinkedList<ConfigItem> lst = new LinkedList<ConfigItem>();
		lst.add(item);
		threadContext().getUser().itemRequireThese(lst, AdminUser.AUTH_READ);

		if (node == null || Objects.equals("", node) || Objects.equals("!any!", node))
			return CfgContextEnvironmentAware.getSharedInstance().inspect(itemCode);

		return ((RabbitmqUpdater) CfgContextEnvironmentAware.publisher).inspect(node, itemCode);
	}

	Object doConfigHistory(HttpServletRequest req) {
		PagerResult<List<ConfigChangeItem>> ret = CfgContextEnvironmentAware.configSource
				.changeHistory(CfgHelper.jsonRead(readBodyAsString(req), PageArgs.class));

		AdminUser user = threadContext().getUser();
		for (ConfigChangeItem item : ret.list) {
			item.changed_content = user.changeFilteredByThese(item.changed_content, AdminUser.AUTH_READ);
		}

		return ret;
	}

	Object doConfigList(HttpServletRequest req) {
		List<ConfigItem> list = CfgContextEnvironmentAware.configSource.configList();

		return threadContext().getUser().itemFilteredBy(list, AdminUser.AUTH_READ, true);

	}

	Object doRestore(HttpServletRequest req, String command) {
		threadContext().put("clientIp", getRemoteHost(req));

		RestoreArgs resargs = CfgHelper.jsonRead(readBodyAsString(req), RestoreArgs.class);

		UpdateSourceReturn ret = CfgContextEnvironmentAware.configSource.restoreChange(resargs.changeId,
				"/restore-version".equals(command), !"/restore-change-after".equals(command), resargs.remark);

		if (ret != null)
			CfgContextEnvironmentAware.publisher.publish(((UpdateSourceReturn) ret).updatedResult, null);

		return ret;
	}

	// plan | store | publish
	Object doChangeSubmit(HttpServletRequest req) {
		threadContext().put("clientIp", getRemoteHost(req));

		MySqlConfigSource src = CfgContextEnvironmentAware.configSource;

		ChangeSubmitArgs args = CfgHelper.jsonRead(readBodyAsString(req), ChangeSubmitArgs.class);

		Object ret = src.submitChange(args);

		if (args.action == ChangeStatus.published) {
			CfgContextEnvironmentAware.publisher.publish(((UpdateSourceReturn) ret).updatedResult, null);
		}

		return ret;
	}

	// cancel | store | publish
	Object doChangeManage(HttpServletRequest req) {
		threadContext().put("clientIp", getRemoteHost(req));

		ChangeManageArgs args = CfgHelper.jsonRead(readBodyAsString(req), ChangeManageArgs.class);

		Object ret = CfgContextEnvironmentAware.configSource.manageChange(args);

		if (args.action == ChangeStatus.published) {
			CfgContextEnvironmentAware.publisher.publish(((UpdateSourceReturn) ret).updatedResult, null);
		}
		return ret;
	}

//	ConfigChangeItem doChangeGet(HttpServletRequest req) {
//		int changedId = Integer.parseInt(req.getParameter("id").toString());
//		MySqlConfigSource src = CfgContextEnvironmentAware.configSource;
//		return src.loadChangeItem(changedId);
//	}

	static private class GrayPublishArgs {
		public int changeId;
		public ChangeStatus status;// graying | terminated | completed ;
		public List<String> list = new LinkedList<String>();
	}

	Object doGrayPublish(HttpServletRequest req) {

		GrayPublishArgs grayArgs = CfgHelper.jsonRead(readBodyAsString(req), GrayPublishArgs.class);

		MySqlConfigSource src = CfgContextEnvironmentAware.configSource;
		ConfigChangeItem changeItem = src.loadChangeItem(grayArgs.changeId);

		AdminUser user = threadContext().getUser();
		user.changeRequire(Arrays.asList(changeItem.changed_content), AdminUser.AUTH_PUBLISH, true);

		List<ConfigItemCore> list = src.changeResult(changeItem.changed_content);

		RabbitmqUpdater updater = (RabbitmqUpdater) CfgContextEnvironmentAware.publisher;

		updater.publish(list, grayArgs.list);

		ConfigChangeItem chg = src.loadChangeItem(grayArgs.changeId);
		chg.changed_summary.addNodes(grayArgs.list);
		chg.changed_summary.addRemark(grayArgs.status, "gray publish");
		chg.withStatus(grayArgs.status);
		src.updateChangeItem(chg);

		return chg;
	}

	HashMap<String, Object> configContext(HttpServletRequest req) {
		HashMap<String, Object> context = new HashMap<String, Object>();

		context.put("securityMode", CfgContextEnvironmentAware.getSharedInstance().securityMode.toString());

		AdminUser user = (AdminUser) req.getSession().getAttribute(SysProps.USER_SESSION_KEY);
		if (user != null) {
			context.put("user", user);
			// todo remove these below
//			context.put("username", user.username);
//			context.put("func", user.func);
		}

		String updater = CfgContextEnvironmentAware.publisher instanceof RabbitmqUpdater ? "rabbitmq" : "polling";
		context.put("updater", updater);

		context.put("appNode", CfgContextEnvironmentAware.nodeName());
		return context;

	}

	Object doGetProp(HttpServletRequest req) {
		boolean frontendEnabled = CfgContextEnvironmentAware.environment.getProperty("easyconfig.frontend.enabled",
				boolean.class, true);
		
		if (!frontendEnabled)
			return null;

		String code = req.getParameter("code").trim();
		if (CfgContextEnvironmentAware.environment.containsProperty(code)) {
			String propTxt = CfgContextEnvironmentAware.environment.getProperty(code);

			ConfigItem item = CfgContextEnvironmentAware.configSource.loadConfigItem(code);

			if (item == null)
				return propTxt;

			String type = (String) item.item_schema.get("type");
			if (type == null)
				type = "string";

			if (Objects.equals("string", type)) {
				return propTxt;
			} else if (StringUtils.hasLength(propTxt))
				return CfgHelper.jsonRead(propTxt, Object.class);
			else
				return null;

		} else {
			throw new RuntimeException("Property is not existed.");
		}
	}

	String doChangePassword(HttpServletRequest req) {
		if (CfgContextEnvironmentAware.getSharedInstance().securityMode != SecurityMode.self) {
			throw new ApplicationException("this action is valid only if easyconfig.security-mode equals [self].");
		}

		Map<String, Object> args = CfgHelper.jsonRead(readBodyAsString(req));
		String oldpass = (String) args.get("oldpass");
		String newpass = (String) args.get("newpass");

		AdminUser user = (AdminUser) req.getSession().getAttribute(SysProps.USER_SESSION_KEY);

		if (user == null) {
			throw new RuntimeException("Not login.");
		}

		if (!CfgHelper.hash(oldpass + user.salt).equals(user.password)) {
			throw new RuntimeException("Incorrect password.");
		}

		user.password = CfgHelper.hash(newpass + user.salt);

		ConfigItem item = CfgContextEnvironmentAware.configSource.loadConfigItem(SysProps.USERS_PROP_KEY);

		AdminUser[] lst = CfgHelper.jsonRead(item.item_value, AdminUser[].class);
		for (AdminUser u : lst) {
			if (user.username.equals(u.username)) {
				u.password = user.password;
				break;
			}
		}

		item.item_value = CfgHelper.jsonWrite(lst);
		CfgContextEnvironmentAware.configSource.updateConfigItem(item);

		return "Password is updated.";
	}

	List<Map<String, Object>> doAdminUserList() {
		threadContext().getUser().funcRequireThese(AdminUser.FUNC_ADMIN);

		ConfigItem item = CfgContextEnvironmentAware.configSource.loadConfigItem(SysProps.USERS_PROP_KEY);

		@SuppressWarnings("unchecked")
		List<Map<String, Object>> lst = CfgHelper.jsonRead(item.item_value, List.class);

		return trimUserList(lst);
	}

	AdminUser[] doAdminUserCreate(AdminUser user) {
		threadContext().getUser().funcRequireThese(AdminUser.FUNC_ADMIN);
		user.salt = ((Integer) new Random().nextInt()).toString();
		user.password = CfgHelper.hash(user.password + user.salt);

		ConfigItem item = CfgContextEnvironmentAware.configSource.loadConfigItem(SysProps.USERS_PROP_KEY);

		AdminUser[] lst = CfgHelper.jsonRead(item.item_value, AdminUser[].class);
		lst = Arrays.copyOf(lst, lst.length + 1);
		lst[lst.length - 1] = user;

		Arrays.<AdminUser>sort(lst, (o1, o2) -> {
			return o1.username.compareTo(o2.username);
		});

		item.item_value = CfgHelper.jsonWrite(lst);

		CfgContextEnvironmentAware.configSource.updateConfigItem(item);

		return trimUserArray(lst);
	}

	private static List<Map<String, Object>> trimUserList(List<Map<String, Object>> list) {
		for (Map<String, Object> user : list) {
			user.remove("salt");
			user.remove("password");
		}
		return list;
	}

	private static AdminUser[] trimUserArray(AdminUser[] array) {
		for (AdminUser user : array) {
			user.salt = null;
			user.password = null;
		}
		return array;
	}

	AdminUser[] doAdminUserPut(HttpServletRequest req) {
		threadContext().getUser().funcRequireThese(AdminUser.FUNC_ADMIN);
		AdminUser newUser = CfgHelper.jsonRead(readBodyAsString(req), AdminUser.class);

		ConfigItem configItem = CfgContextEnvironmentAware.configSource.loadConfigItem(SysProps.USERS_PROP_KEY);
		AdminUser[] userList = CfgHelper.jsonRead(configItem.item_value, AdminUser[].class);

		AdminUser oldUser = null;
		for (AdminUser u : userList) {
			if (u.username.equals(newUser.username)) {
				oldUser = u;
				break;
			}
		}

		if (oldUser != null) {
			oldUser.buildFrom(newUser);
		} else {
			userList = Arrays.copyOf(userList, userList.length + 1);
			userList[userList.length - 1] = new AdminUser().buildFrom(newUser);
		}

		Arrays.<AdminUser>sort(userList, (o1, o2) -> {
			return o1.username.compareTo(o2.username);
		});

		configItem.item_value = CfgHelper.jsonWrite(userList);

		CfgContextEnvironmentAware.configSource.updateConfigItem(configItem);

		// todo clear online current user, remove cookie and session completely
//		AdminUser user = (AdminUser) req.getSession().getAttribute(SysProps.USER_SESSION_KEY);
//		if (user != null && Objects.equals(user.username, newUser.username))
//			req.getSession().removeAttribute(SysProps.USER_SESSION_KEY);

		return trimUserArray(userList);
	}

	List<Map<String, Object>> doAdminUserDelete(Map<String, Object> user) {
		threadContext().getUser().funcRequireThese(AdminUser.FUNC_ADMIN);

		String username = user.get("username").toString();

		ConfigItem item = CfgContextEnvironmentAware.configSource.loadConfigItem(SysProps.USERS_PROP_KEY);

		@SuppressWarnings("unchecked")
		List<Map<String, Object>> lst = CfgHelper.jsonRead(item.item_value, List.class);
		Iterator<Map<String, Object>> it = lst.iterator();
		while (it.hasNext()) {
			if (it.next().get("username").equals(username))
				it.remove();
		}

		item.item_value = CfgHelper.jsonWrite(lst);

		CfgContextEnvironmentAware.configSource.updateConfigItem(item);

		return trimUserList(lst);
	}

	static String getCookie(HttpServletRequest req, String cookieName) {
		Cookie[] cookies = req.getCookies();
		if (cookies == null)
			return null;

		for (int i = 0; i < cookies.length; i++) {
			Cookie c = cookies[i];
			if (c.getName().equals(cookieName)) {
				return c.getValue();
			}
		}

		return null;
	}

	boolean assureLogined(HttpServletRequest req, HttpServletResponse resp) {
		SecurityMode securityMode = CfgContextEnvironmentAware.getSharedInstance().securityMode;
		AdminUser user = (AdminUser) req.getSession().getAttribute(SysProps.USER_SESSION_KEY);

		if (user == null && securityMode == SecurityMode.none) {
			user = AdminUser.unknown;
		}

		if (user == null && securityMode == SecurityMode.self) {
			String usercookie = getCookie(req, SysProps.USER_COOKIE_KEY);
			try {
				if (usercookie != null) {
					String[] remember = CfgHelper.decrypt(usercookie).split(":");
					AdminUser user1 = getUser(remember[0]);

					if (user1 != null && user1.password.equals(remember[1])) {
						user = user1;
						req.getSession().setAttribute(SysProps.USER_SESSION_KEY, user);
					}
				}
			} catch (Throwable e) {
				e.printStackTrace();
				CfgHelper.logger.warn("Error when to decrypt remember me cookie.{}", e.getMessage());
			}

			if (user == null) {
				doResourceResponse("/security-login.html", resp);
			}
		}

		if (user == null && (securityMode == SecurityMode.host || securityMode == SecurityMode.mixture)) {
			String[] names = CfgContextEnvironmentAware.applicationContext.getBeanNamesForType(CfgContextUser.class,
					false, false);
			CfgContextUser contextUser = null;
			if (names.length > 0) {
				contextUser = (CfgContextUser) CfgContextEnvironmentAware.applicationContext.getBean(names[0]);
			}
			if (names.length != 1) {
				CfgHelper.logger.error("Error to find [{}](only one is right) CfgContextUser bean in spring",
						names.length);
			}

			user = securityMode == SecurityMode.host ? contextUser.user() : getUser(contextUser.username());

			if (user == null) {
				throw new AppIllegalAccessException("Invalid or non-existent user access easy config.");
			}

			req.getSession().setAttribute(SysProps.USER_SESSION_KEY, user);

//			if (user == null) {
//				try {
//					RequestDispatcher dispatcher = req.getRequestDispatcher(contextUser.loginUrl());
//					dispatcher.forward(req, resp);
//				} catch (Throwable e) {
//					throw new RuntimeException(e);
//				}
//			}
		}

		threadContext().setUser(user);

		return user != null;
	}

	private AdminUser getUser(String username) {

		ConfigItem item = CfgContextEnvironmentAware.configSource.loadConfigItem(SysProps.USERS_PROP_KEY);
		AdminUser[] lst = CfgHelper.jsonRead(item.item_value, AdminUser[].class);
		for (AdminUser u : lst) {
			if (username.equals(u.username)) {
				return u;
			}
		}
		return null;
	}

	Object doLogin(HttpServletRequest req, HttpServletResponse resp) {
		if (CfgContextEnvironmentAware.getSharedInstance().securityMode != SecurityMode.self) {
			throw new ApplicationException("this action is valid only if easyconfig.security-mode equals [self].");
		}

		Map<String, Object> cmd = CfgHelper.jsonRead(readBodyAsString(req));

		String username = (String) cmd.get("username");
		String password = (String) cmd.get("password");

		AdminUser user = getUser(username);
		if (user == null)
			throw new RuntimeException("Incorrect username or password.");

		if (CfgHelper.hash(password + user.salt).equals(user.password)) {
			req.getSession().setAttribute(SysProps.USER_SESSION_KEY, user);
		} else
			throw new RuntimeException("Incorrect username or password.");

		Boolean rememberMe = (Boolean) cmd.get("rememberMe");
		if (Objects.equals(true, rememberMe)) {
			// spring:Base64(username:expireTime:MD5(username:expireTime:password:secretKey))
			Cookie cookie = new Cookie(SysProps.USER_COOKIE_KEY, CfgHelper.encrypt(username + ":" + user.password));
			cookie.setMaxAge(30 * 24 * 60 * 60);
			cookie.setHttpOnly(true);
			resp.addCookie(cookie);
		}

		return "login success!";
	}

	Object doLogout(HttpServletRequest req, HttpServletResponse resp) {
		if (CfgContextEnvironmentAware.getSharedInstance().securityMode != SecurityMode.self) {
			throw new ApplicationException("this action is valid only if easyconfig.security-mode equals [self].");
		}

		AdminUser user = (AdminUser) req.getSession().getAttribute(SysProps.USER_SESSION_KEY);

		if (user != null)
			req.getSession().removeAttribute(SysProps.USER_SESSION_KEY);

		Cookie cookie = new Cookie(SysProps.USER_COOKIE_KEY, (String) null);
		cookie.setMaxAge(0);
		resp.addCookie(cookie);

		return (user != null ? user.username : "") + " succeed to logout";
	}

	static void doResourceResponse(String resource, HttpServletResponse resp) {
		ClassPathResource res = new ClassPathResource(resource);

		String mimetype = "application/octet-stream";
		switch (getFileExt(resource)) {
		case ".js":
			mimetype = "text/javascript,charset=UTF-8";
			break;
		case ".css":
			mimetype = "text/css,charset=UTF-8";
			break;
		case ".htm":
		case ".html":
			mimetype = "text/html,charset=UTF-8";
			break;
		case ".json":
			mimetype = "application/json,charset=UTF-8";
			break;
		case ".gif":
			mimetype = "image/gif";
			break;
		case ".png":
			mimetype = "image/png";
			break;
		case ".ttf":
			mimetype = "font/x-font-ttf";
			break;
		case ".woff":
			mimetype = "font/x-font-woff";
			break;
		}

		resp.setContentType(mimetype);

		try (InputStream in = res.getInputStream()) {

			inStream2outStream(in, resp.getOutputStream());

		} catch (FileNotFoundException e) {
			// class path resource does not exist
			CfgHelper.logger.warn(e.getMessage());
			resp.setStatus(404);
		} catch (IOException e) {
			CfgHelper.logger.warn("Error when to get {},{}", resource, e.getMessage());
			resp.setStatus(500);
		}
	}

	static Object doCipher(HttpServletRequest req) {
		String jsonText = readBodyAsString(req);
		Map<String, Object> map = CfgHelper.jsonRead(jsonText);
		if (map.get("action").equals("decrypt")) {
			map.put("result", CfgHelper.decrypt(map.get("content").toString()));
		} else if (map.get("action").equals("encrypt")) {
			map.put("result", CfgHelper.encrypt(map.get("content").toString()));
		} else {
			map.put("result", "unknown action");
		}
		return map;
	}

	static Map<String, Object> doSystem(ConfigurableEnvironment env) {
		HashMap<String, Object> envMap = new HashMap<String, Object>();
		envMap.put("activeProfiles", env.getActiveProfiles());
		envMap.put("defaultProfiles", env.getDefaultProfiles());
		envMap.put("systemEnvironment", env.getSystemEnvironment());
		envMap.put("systemProperties", env.getSystemProperties());
		return envMap;
	}

	static void doEnvironment(HttpServletResponse resp) throws Throwable {
		Object result = getEnvForJson(CfgContextEnvironmentAware.environment);
		resp.setContentType("application/json,charset=UTF-8");
		resp.getOutputStream().write(CfgHelper.jsonWrite(result).getBytes(StandardCharsets.UTF_8));
//		ObjectMapper m = new com.fasterxml.jackson.databind.ObjectMapper();
//		m.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
//		m.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
//		resp.getOutputStream().write(m.writerWithDefaultPrettyPrinter().writeValueAsString(result).getBytes(StandardCharsets.UTF_8));
	}

	static private HashMap<String, Object> getEnvForJson(ConfigurableEnvironment env) {
		HashMap<String, Object> envMap = new HashMap<String, Object>();
		envMap.put("activeProfiles", env.getActiveProfiles());
		envMap.put("defaultProfiles", env.getDefaultProfiles());
		envMap.put("systemEnvironment", env.getSystemEnvironment());
		envMap.put("systemProperties", env.getSystemProperties());

		MutablePropertySources propertySources = env.getPropertySources();
		java.util.Iterator<PropertySource<?>> propertySourceIterator = propertySources.iterator();
		LinkedList<Object> propertySourceOrders = new LinkedList<Object>();
		while (propertySourceIterator.hasNext()) {
			PropertySource<?> propertySource = propertySourceIterator.next();

			HashMap<String, Object> sourceMap = new HashMap<String, Object>();

			String sourceName = propertySource.getName();
			sourceMap.put("name", sourceName);
			String clazz = propertySource.getClass().getName();
			sourceMap.put("class", clazz);

			if (propertySource instanceof MapPropertySource) {
				sourceMap.put("source", propertySource.getSource());

				envMap.put(sourceName, sourceMap);

				propertySourceOrders.addLast(sourceName);
				continue;
			}

			switch (clazz) {
			case "org.springframework.core.env.SimpleCommandLinePropertySource":
			case "org.springframework.boot.env.RandomValuePropertySource":
			case "org.springframework.core.env.PropertySource$StubPropertySource":
				sourceMap.put("source", propertySource.getSource());

				envMap.put(sourceName, sourceMap);
				propertySourceOrders.addLast(sourceName);
				break;
			case "org.springframework.web.context.support.ServletContextPropertySource":
				ServletContext sc = ((ServletContextPropertySource) propertySource).getSource();
				HashMap<String, String> scMap = new HashMap<String, String>();
				Enumeration<String> names = sc.getAttributeNames();
				while (names.hasMoreElements()) {
					String name = names.nextElement();
					Object object = sc.getAttribute(name);
					scMap.put(name, object == null ? "" : object.toString());
				}
				sourceMap.put("source", scMap);

				envMap.put(sourceName, sourceMap);
				propertySourceOrders.addLast(sourceName);
				break;
			case "org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertySource":
//				List<Object> list = new LinkedList<Object>();
//				try {
//					Iterable<?> iter = (Iterable<?>) propertySource.getClass().getMethod("getSource")
//							.invoke(propertySource);
//					iter.forEach(it -> {
//						try {
//							Object o = it.getClass().getMethod("getUnderlyingSource").invoke(it);
//							list.add(o);
//						} catch (Throwable e) {
//							e.printStackTrace();
//						}
//					});
//
//				} catch (Throwable e) {
//					e.printStackTrace();
//				}
//				sourceMap.put("source", list);
			default:
				sourceMap.put("source", propertySource.getSource().toString());
				sourceMap.put("notice:", "not returned");
				envMap.put(sourceName, sourceMap);
				propertySourceOrders.addLast(sourceName);
				CfgHelper.logger.info("{} is not returned", clazz);
			}
		}

		envMap.put("propertySources.order", propertySourceOrders);
		return envMap;
	}

	// resource's extension file with '.'
	static String getFileExt(String path) {
		int start = path.lastIndexOf(".");

		return start == -1 ? "" : path.substring(start).toLowerCase();
	}

	static String readBodyAsString(HttpServletRequest request) {
		StringBuilder sb = new StringBuilder("");
		try (BufferedReader br = request.getReader()) {
			String str;
			while ((str = br.readLine()) != null) {
				sb.append(str);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return sb.toString();
	}

	static void inStream2outStream(InputStream in, OutputStream out) throws IOException {
		int len = 0;
		byte[] bys = new byte[1024];
		while ((len = in.read(bys)) != -1) {
			out.write(bys, 0, len);
		}
	}

	@SuppressWarnings({ "unused", "deprecation" })
	static String getRemoteHost(HttpServletRequest request) {
		// reverse proxy
		String STR_UNKNOWN = "unknown";
		String ip = request.getHeader("X-Real-IP");
		if (StringUtils.isEmpty(ip) || STR_UNKNOWN.equalsIgnoreCase(ip)) {
			ip = request.getHeader("x-Forwarded-For");
		}

		if (StringUtils.isEmpty(ip) || STR_UNKNOWN.equalsIgnoreCase(ip)) {
			ip = request.getHeader("Proxy-Client-IP");
		}

		if (StringUtils.isEmpty(ip) || STR_UNKNOWN.equalsIgnoreCase(ip)) {
			ip = request.getHeader("WL-Proxy-Client-IP");
		}

		if (StringUtils.isEmpty(ip) || STR_UNKNOWN.equalsIgnoreCase(ip)) {
			ip = request.getRemoteAddr();
		}
		return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
	}

}
